build: bump openapi-generation to license token validation and wire the license election - #2124
build: bump openapi-generation to license token validation and wire the license election#2124ThomasRooney wants to merge 1 commit into
Conversation
|
Running ultrareview automatically — High-risk licensing change: wiring the CLI to the new license-token validation changes AGPL vs commercial election across all generation paths; a missed bug could mislicense outputs or break authenticated runs.. I'll post findings when complete. |
There was a problem hiding this comment.
Ultrareview completed in 7m 55s
All reported issues were addressed across 6 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
…2126) Dependency bump only, split out of #2124 so the SDK/core change ships and is verified on its own before the generator change. - `speakeasy-client-sdk-go/v3` v3.28.0 → **v3.28.1** (regenerated from the registry spec after speakeasy-registry#4708: `AccessDetails` gains `license_jwt` and `status_code`; no signature changes). - `speakeasy-core` v0.23.0 → **v0.24.0** (`access.CheckGenerationAccess` returns the full access decision including the platform-issued license token; `HasGenerationAccess` unchanged). No CLI behaviour change: nothing reads the token yet. #2124 (generator pin + commercial-on-token wiring) rebases on top once openapi-generation#55 is released. Guards from #2121 still apply: `internal/sdk/sdk_test.go` asserts `x-api-key` on `Artifacts.PostTags`/`GetRevisions`; `TestRegistryFlow` ends with `tag promote`. <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Bumps `speakeasy-core` to v0.24.0 and `speakeasy-client-sdk-go/v3` to v3.28.1. These upgrades expose a platform-issued license token and new `AccessDetails` fields (`license_jwt`, `status_code`), but nothing reads them yet, so CLI behavior is unchanged. <sup>Written for commit 943cae8. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/speakeasy-api/speakeasy/pull/2126?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
5dbba26 to
07519df
Compare
There was a problem hiding this comment.
All reported issues were addressed across 6 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 13 files (changes from recent commits).
Tip: instead of fixing issues one by one fix them all with cubic
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 17 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 15 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
588a056 to
532bfa2
Compare
There was a problem hiding this comment.
2 issues found across 15 files (changes from recent commits).
Confidence score: 2/5
internal/model/command.goconflates--forceforrunandgenerate sdkwith an authentication refresh, causing valid offline licenses to be bypassed; keep generation forcing separate from authentication refresh handling.internal/auth/auth.goreturns an SDK-less context for valid offline-token users on everyRequiresAuthcommand, sostatus,billing activate, and configure can fail despite valid credentials; ensure offline authentication supplies the required SDK context or narrowly scopes the restriction.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="internal/auth/auth.go">
<violation number="1" location="internal/auth/auth.go:74">
P2: When a valid offline token is used without an API key, `CommandContext` returns an SDK-less context for every `RequiresAuth` command. Commands such as `status`, `billing activate`, and configure then fail with a missing-API-key error instead of authenticating online; restrict this path to offline-capable commands or ensure platform authentication before online commands.
(Based on your team's feedback about SDK-less offline command contexts.)</violation>
</file>
<file name="internal/model/command.go">
<violation number="1" location="internal/model/command.go:140">
P1: When `--force` is used on `run` or `generate sdk`, this treats the generation-control flag as an authentication refresh request and bypasses valid offline licenses. Keep generation force separate from authentication refresh, using a dedicated auth-refresh signal instead.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Confidence score: 4/5
- In
cmd/lint/lint.go, unauthenticated interactive lint runs can swallow theWithGenerationContexterror and show an empty generation-warnings tab as though the dry run succeeded; return the error so callers can report the failure.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="cmd/lint/lint.go">
<violation number="1" location="cmd/lint/lint.go:603">
P2: When lint runs interactively without authentication, `WithGenerationContext` fails and this returns a nil error, so the caller displays an empty generation-warnings tab as if the dry run succeeded. Return the error here; the callers already skip targets whose dry run returns an error.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| licenseToken, _ := coreauth.GetLicenseTokenFromContext(ctx) | ||
| commercialCtx, err := coreauth.WithGenerationContext(ctx, generationaccess.GeneratedLicenseCommercial) | ||
| if err != nil { | ||
| return nil, nil |
There was a problem hiding this comment.
P2: When lint runs interactively without authentication, WithGenerationContext fails and this returns a nil error, so the caller displays an empty generation-warnings tab as if the dry run succeeded. Return the error here; the callers already skip targets whose dry run returns an error.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/lint/lint.go, line 603:
<comment>When lint runs interactively without authentication, `WithGenerationContext` fails and this returns a nil error, so the caller displays an empty generation-warnings tab as if the dry run succeeded. Return the error here; the callers already skip targets whose dry run returns an error.</comment>
<file context>
@@ -589,10 +591,18 @@ func warningsToTabContents(warnings []error) []interactivity.InspectableContent
+ licenseToken, _ := coreauth.GetLicenseTokenFromContext(ctx)
+ commercialCtx, err := coreauth.WithGenerationContext(ctx, generationaccess.GeneratedLicenseCommercial)
+ if err != nil {
+ return nil, nil
+ }
+ ctx = licensetoken.WithToken(commercialCtx, licenseToken)
</file context>
| return nil, nil | |
| return nil, err |
There was a problem hiding this comment.
1 issue found across 5 files (changes from recent commits).
Confidence score: 2/5
internal/sdkgen/sdkgen.gocan preserve an old token after tokenless online re-authentication and skip the platform access check when a workspace is downgraded or blocked, potentially allowing access that should be denied; clear the old token during tokenless re-authentication.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="internal/sdkgen/sdkgen.go">
<violation number="1" location="internal/sdkgen/sdkgen.go:374">
P1: When a workspace is downgraded or blocked after tokenless online re-authentication, this branch still treats the preserved old token as sufficient and skips the platform access check. Clear the old token on tokenless re-authentication or retain the access check whenever an SDK is present, so blocked generation cannot proceed.
(Based on your team's feedback about stale offline licenses after downgrade.)</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| // An offline-license context still carries an SDK client when an API key is | ||
| // persisted; the license decides generation access there, so the platform | ||
| // access check (and therefore the network) must not be required. | ||
| if hasOfflineLicense(ctx) && len(licenseToken) > 0 { |
There was a problem hiding this comment.
P1: When a workspace is downgraded or blocked after tokenless online re-authentication, this branch still treats the preserved old token as sufficient and skips the platform access check. Clear the old token on tokenless re-authentication or retain the access check whenever an SDK is present, so blocked generation cannot proceed.
(Based on your team's feedback about stale offline licenses after downgrade.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/sdkgen/sdkgen.go, line 374:
<comment>When a workspace is downgraded or blocked after tokenless online re-authentication, this branch still treats the preserved old token as sufficient and skips the platform access check. Clear the old token on tokenless re-authentication or retain the access check whenever an SDK is present, so blocked generation cannot proceed.
(Based on your team's feedback about stale offline licenses after downgrade.) </comment>
<file context>
@@ -367,6 +368,12 @@ func evaluateGenerationAccess(ctx context.Context, args *access.GenerationAccess
+ // An offline-license context still carries an SDK client when an API key is
+ // persisted; the license decides generation access there, so the platform
+ // access check (and therefore the network) must not be required.
+ if hasOfflineLicense(ctx) && len(licenseToken) > 0 {
+ return ctx, &access.GenerationAccess{Allowed: true}, licenseToken, nil
+ }
</file context>
…commercial license election and offline licenses Bumps openapi-generation to a version that validates commercial license tokens and requires an explicit license election for generated output. The CLI always elects the customer's commercial license: the AGPL election exists only as a source-build fallback in the upstream generator and is never used here. A missing license token no longer downgrades the election; generation proceeds as commercial and the generator rejects it as unproven, with a CLI warning pointing at the platform. Lint's diagnostic dry-run reuses the workspace license token under a commercial election and skips diagnostics when unauthenticated. Adds offline licenses (internal/license): a validated license token from SPEAKEASY_LICENSE_TOKEN, SPEAKEASY_LICENSE_FILE, or the persisted offline_license_token authenticates 'run' and 'generate sdk' without the platform, gated per command by the new OfflineCapable field. Offline contexts re-authenticate on demand via EnsureTargets, EnsurePlatform, and WithPlatformFallback, so GitHub runs and uncovered targets still go online. Generation access honors the offline license even when a persisted API key put an SDK client in the context, keeping offline generation offline for logged-in users. Auth persistence captures the license token issued at validation, scoped to the authenticated workspace and never for free tiers. Token-less authentication preserves a manually configured offline license, a workspace change clears it, and logout wipes it. --force on run and generate sdk stays a generation-control flag; refreshing the persisted license online is done explicitly via 'speakeasy auth login', which can again replace a revoked key or switch accounts.
8c57c76 to
ba6ef5c
Compare
There was a problem hiding this comment.
4 issues found across 17 files (changes from recent commits).
Confidence score: 2/5
internal/run/run.goinWorkflow.Runskips the explicit AGPL election without workspace context, allowing tokenless workflow runs to enter generation incorrectly — restore the no-workspace AGPL election before target checks.internal/sdkgen/sdkgen.goinwithGenerationContextelects the commercial license when no token is issued, causing the validator to reject required AGPL fallback generation — select AGPL for tokenless generation.cmd/lint/lint.golets unauthenticatedlint openapi --dry-runattempt commercial generation with an empty token, while discarded errors can make the dry run fail silently — use the appropriate AGPL fallback and propagate or surface generation errors.internal/auth/auth.goforces browser opening after offline-license checks return 401/403 instead of retrying with the configured API key, breaking automatic retries in CI and other non-interactive environments — refresh through the API key first.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="internal/run/run.go">
<violation number="1" location="internal/run/run.go:183">
P1: When `Workflow.Run` has no workspace context, this preparation performs no explicit AGPL/direct election. Restore the no-workspace AGPL election before target checks so tokenless workflow runs do not enter generation with the wrong licensing context.</violation>
</file>
<file name="cmd/lint/lint.go">
<violation number="1" location="cmd/lint/lint.go:601">
P2: When `lint openapi --dry-run` runs without authentication, this branch elects commercial with an empty token. The generator rejects tokenless commercial generation, while `runDryRunGeneration` discards its errors, so unauthenticated lint silently loses target-specific warnings; elect AGPL explicitly when no token is available.</violation>
</file>
<file name="internal/auth/auth.go">
<violation number="1" location="internal/auth/auth.go:106">
P2: When an offline-license access check returns 401/403, this fallback passes `force=true`, which opens the browser instead of refreshing through the configured API key. Automatic retries therefore fail in CI and other non-interactive environments; use a non-interactive online refresh path here.</violation>
</file>
<file name="internal/sdkgen/sdkgen.go">
<violation number="1" location="internal/sdkgen/sdkgen.go:407">
P1: When no license token is issued, `withGenerationContext` still elects the commercial license, so the CLI does not produce the required AGPL fallback and the validator can reject generation. Elect AGPL when `licenseToken` is empty, commercial only when a token is present, and emit the AGPL-3.0-only warning for that branch.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
|
|
||
| if _, err := core.GetWorkspaceIDFromContext(ctx); err != nil { | ||
| return generationaccess.WithDirect(ctx), nil //nolint:nilerr // Direct runs do not carry workspace context. | ||
| ctx, err = ensureTargets(ctx, targetTypes) |
There was a problem hiding this comment.
P1: When Workflow.Run has no workspace context, this preparation performs no explicit AGPL/direct election. Restore the no-workspace AGPL election before target checks so tokenless workflow runs do not enter generation with the wrong licensing context.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/run/run.go, line 183:
<comment>When `Workflow.Run` has no workspace context, this preparation performs no explicit AGPL/direct election. Restore the no-workspace AGPL election before target checks so tokenless workflow runs do not enter generation with the wrong licensing context.</comment>
<file context>
@@ -173,25 +175,48 @@ func (w *Workflow) Run(ctx context.Context) error {
-
- if _, err := core.GetWorkspaceIDFromContext(ctx); err != nil {
- return generationaccess.WithDirect(ctx), nil //nolint:nilerr // Direct runs do not carry workspace context.
+ ctx, err = ensureTargets(ctx, targetTypes)
+ if err != nil {
+ return ctx, err
</file context>
| // commercial; the generator then rejects the generation as unproven rather | ||
| // than silently downgrading the license. | ||
| func withGenerationContext(ctx context.Context, licenseToken []byte) (context.Context, error) { | ||
| ctx, err := coreauth.WithGenerationContext(ctx, generationaccess.GeneratedLicenseCommercial) |
There was a problem hiding this comment.
P1: When no license token is issued, withGenerationContext still elects the commercial license, so the CLI does not produce the required AGPL fallback and the validator can reject generation. Elect AGPL when licenseToken is empty, commercial only when a token is present, and emit the AGPL-3.0-only warning for that branch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/sdkgen/sdkgen.go, line 407:
<comment>When no license token is issued, `withGenerationContext` still elects the commercial license, so the CLI does not produce the required AGPL fallback and the validator can reject generation. Elect AGPL when `licenseToken` is empty, commercial only when a token is present, and emit the AGPL-3.0-only warning for that branch.</comment>
<file context>
@@ -348,12 +361,54 @@ func Generate(ctx context.Context, opts GenerateOptions) (*GenerationAccess, err
+// commercial; the generator then rejects the generation as unproven rather
+// than silently downgrading the license.
+func withGenerationContext(ctx context.Context, licenseToken []byte) (context.Context, error) {
+ ctx, err := coreauth.WithGenerationContext(ctx, generationaccess.GeneratedLicenseCommercial)
+ if err != nil {
+ return ctx, err
</file context>
| if _, ok := generationaccess.StateFromContext(ctx); !ok { | ||
| ctx = generationaccess.WithDirect(ctx) | ||
| licenseToken, _ := coreauth.GetLicenseTokenFromContext(ctx) | ||
| commercialCtx, err := coreauth.WithGenerationContext(ctx, generationaccess.GeneratedLicenseCommercial) |
There was a problem hiding this comment.
P2: When lint openapi --dry-run runs without authentication, this branch elects commercial with an empty token. The generator rejects tokenless commercial generation, while runDryRunGeneration discards its errors, so unauthenticated lint silently loses target-specific warnings; elect AGPL explicitly when no token is available.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cmd/lint/lint.go, line 601:
<comment>When `lint openapi --dry-run` runs without authentication, this branch elects commercial with an empty token. The generator rejects tokenless commercial generation, while `runDryRunGeneration` discards its errors, so unauthenticated lint silently loses target-specific warnings; elect AGPL explicitly when no token is available.</comment>
<file context>
@@ -589,10 +591,18 @@ func warningsToTabContents(warnings []error) []interactivity.InspectableContent
if _, ok := generationaccess.StateFromContext(ctx); !ok {
- ctx = generationaccess.WithDirect(ctx)
+ licenseToken, _ := coreauth.GetLicenseTokenFromContext(ctx)
+ commercialCtx, err := coreauth.WithGenerationContext(ctx, generationaccess.GeneratedLicenseCommercial)
+ if err != nil {
+ return nil, nil
</file context>
| if err == nil || licenseFromContext(ctx) == nil || !isAuthenticationFailure(err) { | ||
| return ctx, err | ||
| } | ||
| authCtx, err := authenticateWithHint(ctx, true) |
There was a problem hiding this comment.
P2: When an offline-license access check returns 401/403, this fallback passes force=true, which opens the browser instead of refreshing through the configured API key. Automatic retries therefore fail in CI and other non-interactive environments; use a non-interactive online refresh path here.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At internal/auth/auth.go, line 106:
<comment>When an offline-license access check returns 401/403, this fallback passes `force=true`, which opens the browser instead of refreshing through the configured API key. Automatic retries therefore fail in CI and other non-interactive environments; use a non-interactive online refresh path here.</comment>
<file context>
@@ -2,31 +2,157 @@ package auth
+ if err == nil || licenseFromContext(ctx) == nil || !isAuthenticationFailure(err) {
+ return ctx, err
+ }
+ authCtx, err := authenticateWithHint(ctx, true)
+ if err != nil {
+ return authCtx, err
</file context>
| authCtx, err := authenticateWithHint(ctx, true) | |
| authCtx, err := authenticateWithHint(ctx, false) |
Summary
Wires the CLI to the platform's target-scoped license tokens (speakeasy-registry#4708, live) and the generator's offline validation (openapi-generation#55).
Pins:
openapi-generation/v2v2.934.0 (openapi-generation#55: offline license validation with target-scoped tokens)speakeasy-corev0.24.0 (access.CheckGenerationAccessexposingAccessDetails.license_jwt)speakeasy-client-sdk-go/v3v3.28.1generation-contextv1.1.0 (WithDirectno longer implies AGPL; explicitElectAGPL())Behaviour
internal/sdkgen: the access check now returns the platform-issued license token (["*"]for paid tiers on every non-blocked check — allowed and warning — one named target for free workspaces). The CLI elects commercial whenever a token is present and attaches it for the generator to validate; the/v1/auth/validatetoken is a fallback. No token ⇒ AGPL election plus a warning ("No license token was issued for this generation; generated output is licensed under AGPL-3.0-only."). Blocked access still aborts before generation, as before.mainit has been AGPL-stamped since the generation-context change). Free workspaces get commercial output for their one language; a second language is blocked by the access check.internal/run: workflow runs without workspace context elect AGPL explicitly.cmd/lint: the dry-run diagnostic generation elects AGPL explicitly (nothing is written).internal/validationunchanged.generate usage/pkg/codesamplesuntouched; they pass the caller's context through.Offline licenses
~/.speakeasy/config.yamlasoffline_license_token(only non-free tokens for the authenticated workspace; cleared on logout or when a re-authentication issues none). Overrides for CI/air-gapped use:SPEAKEASY_LICENSE_TOKEN(raw JWT), thenSPEAKEASY_LICENSE_FILE(path).internal/auth.CommandContext): if it validates (licensetoken.Inspect) and belongs to the configured workspace,/v1/auth/validateis skipped and the authenticated context is built from the token's claims — with an SDK client when an API key is configured, so registry uploads, telemetry and the access check behave exactly as online. Without an API key the run is fully offline (telemetry and registry disabled at context construction, no access check).internal/auth.EnsureTargets, checked once inRunInnerbefore uploads and ingenerate sdk) or an access check that fails with 401/403 (internal/auth.WithPlatformFallback) triggers the normal online authentication, which refreshes the stored token, and the operation is retried once. Nothing falls back to AGPL where the online path would have been commercial.Verified in a sandboxed
$HOMEagainst prod: API key only / garbage token / stale token / other-workspace token / brokenSPEAKEASY_LICENSE_TOKEN→ online path, commercial output, fresh token stored; no API key + valid token → offline commercial; API key + valid token → validate skipped (~3 s saved per command), access check kept.Verification
go build ./..., unit suite (excludingintegration) green, golangci-lint clean on touched packages.run→ commercial output, no warning. Free workspacerun -t go→ commercial output;run -t typescript→ "Upgrade Required … exceeded the limit of one free generated SDK", blocked.